﻿using System;
using System.ComponentModel;
using System.Globalization;
using LukasKopenec.LocalizationSupport;
using Notoric.Commanding.CommandBase;

namespace Notoric.ViewModel
{
    /// <summary>
    /// Abstract base class for positionable and resizable elements.
    /// </summary>
    public abstract class PositionableElement : ResizableElement, IPositionable
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="PositionableElement"/> instance.
        /// </summary>
        /// <param name="adaptedElement">
        /// <para>
        /// Type: <see cref="Model.Elements.ModelElement"/>
        /// </para>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="ViewModelElement"/>
        /// </para>
        /// <para>
        /// The element that directly owns this element.
        /// </para>
        /// </param>
        /// <para>
        /// The adapted model element instance.
        /// </para>
        /// </param>
        /// <param name="undoStack">
        /// <para>
        /// Type: <see cref="CommandStack"/>
        /// </para>
        /// <para>
        /// The undo stack for this element instance.
        /// </para>
        /// </param>
        /// <param name="redoStack">
        /// <para>
        /// Type: <see cref="CommandStack"/>
        /// </para>
        /// <para>
        /// The redo stack for this element instance.
        /// </para>
        /// </param>
        /// <param name="addOwnerOffset">
        /// <para>
        /// Type: <see cref="Boolean"/>
        /// </para>
        /// <para>
        /// Indicates whether the coordinates of the <paramref name="owner"/> will be added to this element's coordinates or not.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="adaptedElement"/>, <paramref name="undoStack"/> or <paramref name="redoStack"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// <paramref name="adaptedElement"/> does not implement the <see cref="Model.Elements.IResizableElement"/> 
        /// or <see cref="Model.Elements.IPositionableElement"/> interface.<br/>
        /// -or-<br/>
        /// <paramref name="addOwnerOffset"/> is <see langword="true"/> and <paramref name="owner"/> is not a <see cref="PositionableElement"/>.
        /// </exception>
        public PositionableElement(Model.Elements.ModelElement adaptedElement, ViewModelElement owner, CommandStack undoStack, CommandStack redoStack, bool addOwnerOffset)
            : base(adaptedElement, owner, undoStack, redoStack)
        {
            if (!(adaptedElement is Model.Elements.IPositionableElement))
                throw new ArgumentException("The element does not implement the IPositionableElement interface!", "adaptedElement");
            if (addOwnerOffset)
            {
                if (owner == null)
                    throw new ArgumentNullException("owner");
                if (!(owner is PositionableElement))
                    throw new ArgumentException("The owner is not a PositionableElement");

                owner.PropertyChanged += new PropertyChangedEventHandler(owner_PropertyChanged);

                offsetX = ((PositionableElement)Owner).Left;
                offsetY = ((PositionableElement)Owner).Top;
            }
            else
            {
                offsetX = 0.0;
                offsetY = 0.0;
            }
        }

        #endregion

        #region ViewModelElement Members

        /// <inheritdoc/>
        public override void Dispose()
        {
            Owner.PropertyChanged -= owner_PropertyChanged;
            base.Dispose();
        }

        #endregion

        #region Event Handlers

        /// <summary>
        /// Occurs when the value of some property of the <see cref="ViewModelElement.Owner"/> has changed.
        /// </summary>
        /// <remarks>
        /// <b>Operation:</b><br/>
        /// This handler is invoked if and only if the <see cref="ViewModelElement.Owner"/> is a <see cref="PositionableElement"/> (see the constructor for details).<br/>
        /// If the property that has changed is either Left or Top, the handler caches the new value in the offsetX or offsetY field and raises the 
        /// <see cref="ViewModelElement.PropertyChanged"/> event for the corresponding property of this element.
        /// </remarks>
        /// <param name="sender">Object that has sent the event</param>
        /// <param name="e">Event data</param>
        private void owner_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "Left")
            {
                offsetX = ((PositionableElement)Owner).Left;
                RaisePropertyChanged("Left");
            }
            if (e.PropertyName == "Top")
            {
                offsetY = ((PositionableElement)Owner).Top;
                RaisePropertyChanged("Top");
            }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Creates an undoable command that sets the position of the left edge of this element.
        /// </summary>
        /// <param name="left">
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The position of the left element's edge.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="UndoableCommandBase"/>
        /// </para>
        /// <para>
        /// The created command ready for execution.
        /// </para>
        /// </returns>
        public UndoableCommandBase CreateSetLeftCommand(double left)
        {
            left = (double)LengthConverter.ConvertBack(left - offsetX, typeof(double), null, CultureInfo.CurrentCulture);

            return new SetterCommand<double>(UndoStack, RedoStack)
            {
                Property = "Left",
                Target = this,
                Value = left,
                Name = (string)LocalizationManager.GetValue("Commands.Names.SetPosition")
            };
        }

        /// <summary>
        /// Creates an undoable command that sets the position of this element.
        /// </summary>
        /// <param name="left">
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The value of the <see cref="Left"/> property.
        /// </para>
        /// </param>
        /// <param name="top">
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The value of the <see cref="Top"/> property.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="UndoableCommandBase"/>
        /// </para>
        /// <para>
        /// The created command ready for execution.
        /// </para>
        /// </returns>
        public UndoableCommandBase CreateSetPositionCommand(double left, double top)
        {
            left = (double)LengthConverter.ConvertBack(left - offsetX, typeof(double), null, CultureInfo.CurrentCulture);
            top = (double)LengthConverter.ConvertBack(top - offsetY, typeof(double), null, CultureInfo.CurrentCulture);

            return new SetterCommand<double, double>(UndoStack, RedoStack)
            {
                Property1 = "Left",
                Property2 = "Top",
                Target = this,
                Value1 = left,
                Value2 = top,
                Name = (string)LocalizationManager.GetValue("Commands.Names.SetPosition")
            };
        }

        /// <summary>
        /// Creates an undoable command that sets the position of the top edge of this element.
        /// </summary>
        /// <param name="top">
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The position of the top element's edge.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="UndoableCommandBase"/>
        /// </para>
        /// <para>
        /// The created command ready for execution.
        /// </para>
        /// </returns>
        public UndoableCommandBase CreateSetTopCommand(double top)
        {
            top = (double)LengthConverter.ConvertBack(top - offsetY, typeof(double), null, CultureInfo.CurrentCulture);

            return new SetterCommand<double>(UndoStack, RedoStack)
            {
                Property = "Top",
                Target = this,
                Value = top,
                Name = (string)LocalizationManager.GetValue("Commands.Names.SetPosition")
            };
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets or sets the x-coordinate of the element's top-left corner.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The x-coordinate (in device independent pixels) of the element's top-left corner.<br/>
        /// Depending on the mode of creation this value may or may not include the coordinates of the owner.
        /// </para>
        /// </value>
        public double Left
        {
            get 
            {
                double left;

                if (UnsavedChanges.ContainsKey("Left"))
                {
                    var cmd = (SetterCommand<double>)UnsavedChanges["Left"];
                    left = cmd.Value;
                }
                else
                    left = ((Model.Elements.IPositionableElement)AdaptedElement).Left;

                return offsetX + (double)LengthConverter.Convert(left, typeof(double), null, CultureInfo.CurrentCulture);
            }
            set 
            {
                if (UnsavedChanges.ContainsKey("Left"))
                {
                    var cmd = (SetterCommand<double>)UnsavedChanges["Left"];
                    cmd.Value = (double)LengthConverter.ConvertBack(value - offsetX, typeof(double), null, CultureInfo.CurrentCulture);
                }
                else
                    UnsavedChanges["Left"] = CreateSetLeftCommand(value);

                RaisePropertyChanged("Left");
            }
        }

        /// <summary>
        /// Gets or sets the y-coordinate of the element's top-left corner.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The y-coordinate (in device independent pixels) of the element's top-left corner.<br/>
        /// Depending on the mode of creation this value may or may not include the coordinates of the owner.
        /// </para>
        /// </value>
        public double Top
        {
            get
            {
                double top;

                if (UnsavedChanges.ContainsKey("Top"))
                {
                    var cmd = (SetterCommand<double>)UnsavedChanges["Top"];
                    top = cmd.Value;
                }
                else
                    top = ((Model.Elements.IPositionableElement)AdaptedElement).Top;

                return offsetY + (double)LengthConverter.Convert(top, typeof(double), null, CultureInfo.CurrentCulture);
            }
            set
            {
                if (UnsavedChanges.ContainsKey("Top"))
                {
                    var cmd = (SetterCommand<double>)UnsavedChanges["Top"];
                    cmd.Value = (double)LengthConverter.ConvertBack(value - offsetY, typeof(double), null, CultureInfo.CurrentCulture);
                }
                else
                    UnsavedChanges["Top"] = CreateSetTopCommand(value);

                RaisePropertyChanged("Top");
            }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Caches the owner's x coordinate.
        /// </summary>
        private double offsetX;
        /// <summary>
        /// Caches the owner's y coordinate.
        /// </summary>
        private double offsetY;

        #endregion
    }
}
